import process from 'node:process';
import {Buffer} from 'node:buffer';
import {promisify} from 'node:util';
import stream from 'node:stream';
import {pipeline as streamPipeline} from 'node:stream/promises';
import fs from 'node:fs';
// @ts-expect-error Fails to find slow-stream/index.d.ts
import SlowStream from 'slow-stream';
import getStream from 'get-stream';
import FormData from 'form-data';
import {temporaryFile} from 'tempy';
import is from '@sindresorhus/is';
import test, {type ExecutionContext} from 'ava';
import type {Handler} from 'express';
import {pEvent} from 'p-event';
import {chunk, chunkFromAsync} from 'chunk-data';
import type {Progress} from '../source/index.js';
import withServer from './helpers/with-server.js';

const checkEvents = (t: ExecutionContext, events: Progress[], bodySize?: number) => {
	t.true(events.length >= 2);

	let lastEvent = events.shift()!;

	if (!is.number(bodySize)) {
		t.is(lastEvent.percent, 0);
	}

	for (const [index, event] of events.entries()) {
		const isLastEvent = index === events.length - 1;

		if (is.number(bodySize)) {
			t.is(event.percent, event.transferred / bodySize);
			t.true(event.percent > lastEvent.percent);
			t.true(event.transferred > lastEvent.transferred);
		} else if (isLastEvent) {
			t.is(event.percent, 1);
			t.is(event.transferred, lastEvent.transferred);
			t.is(event.total, event.transferred);
		} else {
			t.is(event.percent, 0);
			t.true(event.transferred > lastEvent.transferred);
		}

		lastEvent = event;
	}
};

const file = Buffer.alloc(1024 * 1024 * 2);

const downloadEndpoint: Handler = (_request, response) => {
	response.setHeader('content-length', file.length);

	(async () => {
		try {
			await streamPipeline(
				stream.Readable.from(file),
				new SlowStream({maxWriteInterval: 50}),
				response,
			);
		} catch {}

		response.end();
	})();
};

const noTotalEndpoint: Handler = (_request, response) => {
	response.write('hello');
	response.end();
};

const uploadEndpoint: Handler = (request, response) => {
	(async () => {
		try {
			await streamPipeline(
				request,
				new SlowStream({maxWriteInterval: 100}),
			);
		} catch {}

		response.end();
	})();
};

test('download progress', withServer, async (t, server, got) => {
	server.get('/', downloadEndpoint);

	const events: Progress[] = [];

	const {body} = await got({responseType: 'buffer'})
		.on('downloadProgress', event => events.push(event));

	checkEvents(t, events, body.length);
});

test('download progress - missing total size', withServer, async (t, server, got) => {
	server.get('/', noTotalEndpoint);

	const events: Progress[] = [];

	await got('').on('downloadProgress', (event: Progress) => events.push(event));

	t.is(events[0]?.total, undefined);
	checkEvents(t, events);
});

test('download progress - stream', withServer, async (t, server, got) => {
	server.get('/', downloadEndpoint);

	const events: Progress[] = [];

	const stream = got.stream({responseType: 'buffer'})
		.on('downloadProgress', event => events.push(event));

	await getStream(stream);

	checkEvents(t, events, file.length);
});

test('upload progress - file', withServer, async (t, server, got) => {
	server.post('/', uploadEndpoint);

	const events: Progress[] = [];

	await got.post({body: file}).on('uploadProgress', (event: Progress) => events.push(event));

	checkEvents(t, events, file.length);
});

test('upload progress - file stream', withServer, async (t, server, got) => {
	server.post('/', uploadEndpoint);

	const path = temporaryFile();
	fs.writeFileSync(path, file);

	const {size} = await promisify(fs.stat)(path);

	const events: Progress[] = [];

	await got.post({
		body: fs.createReadStream(path),
		headers: {
			'content-length': size.toString(),
		},
	})
		.on('uploadProgress', (event: Progress) => events.push(event));

	checkEvents(t, events, file.length);
});

test('upload progress - form data', withServer, async (t, server, got) => {
	server.post('/', uploadEndpoint);

	const events: Progress[] = [];

	const body = new FormData();
	body.append('key', 'value');
	body.append('file', file);

	const size = await promisify(body.getLength.bind(body))();

	await got.post({body}).on('uploadProgress', (event: Progress) => events.push(event));

	checkEvents(t, events, size);
});

test('upload progress - json', withServer, async (t, server, got) => {
	server.post('/', uploadEndpoint);

	const body = JSON.stringify({key: 'value'});
	const size = Buffer.byteLength(body);
	const events: Progress[] = [];

	await got.post({body}).on('uploadProgress', (event: Progress) => events.push(event));

	checkEvents(t, events, size);
});

test('upload progress - measures bytes correctly for non-UTF-8 encoded strings', withServer, async (t, server, got) => {
	server.post('/', uploadEndpoint);

	// String with emoji - 'Hello 👋' is 10 bytes in UTF-8, but 16 bytes in UTF-16LE
	const text = 'Hello 👋';
	const utf8Size = Buffer.from(text, 'utf8').byteLength; // 10 bytes
	const utf16Size = Buffer.from(text, 'utf16le').byteLength; // 16 bytes

	t.is(utf8Size, 10);
	t.is(utf16Size, 16);

	const events: Progress[] = [];

	// Upload as UTF-8 (default)
	await got.post({body: text}).on('uploadProgress', (event: Progress) => events.push(event));

	// Verify the progress measures UTF-8 bytes correctly
	const finalEvent = events.at(-1)!;
	t.is(finalEvent.transferred, utf8Size);
	t.is(finalEvent.total, utf8Size);
});

test('upload progress - stream with known body size', withServer, async (t, server, got) => {
	server.post('/', uploadEndpoint);

	const events: Progress[] = [];
	const options = {
		headers: {'content-length': file.length.toString()},
	};

	const request = got.stream.post(options)
		.on('uploadProgress', event => {
			events.push(event);
		});

	await streamPipeline(stream.Readable.from(file), request);
	await getStream(request);

	checkEvents(t, events, file.length);
});

test('upload progress - stream with unknown body size', withServer, async (t, server, got) => {
	server.post('/', uploadEndpoint);

	const events: Progress[] = [];

	const request = got.stream.post('')
		.on('uploadProgress', event => {
			events.push(event);
		});

	await streamPipeline(stream.Readable.from(file), request);
	await getStream(request);

	t.is(events[0]?.total, undefined);
	checkEvents(t, events);
});

test('upload progress - no body', withServer, async (t, server, got) => {
	server.post('/', uploadEndpoint);

	const events: Progress[] = [];

	await got.post('').on('uploadProgress', (event: Progress) => events.push(event));

	t.deepEqual(events, [
		{
			percent: 0,
			transferred: 0,
			total: undefined,
		},
		{
			percent: 1,
			transferred: 0,
			total: 0,
		},
	]);
});

test('upload progress - no events when immediatly removed listener', withServer, async (t, server, got) => {
	server.post('/', uploadEndpoint);

	const events: Progress[] = [];

	const listener = (event: Progress) => events.push(event);

	const promise = got.post('')
		.on('uploadProgress', listener)
		.off('uploadProgress', listener);

	await promise;

	t.is(events.length, 0);
});

test('upload progress - one event when removed listener', withServer, async (t, server, got) => {
	server.post('/', uploadEndpoint);

	const events: Progress[] = [];

	const promise = got.post('');

	const listener = (event: Progress) => {
		events.push(event);
		void promise.off('uploadProgress', listener);
	};

	void promise.on('uploadProgress', listener);

	await promise;

	t.deepEqual(events, [
		{
			percent: 0,
			transferred: 0,
			total: undefined,
		},
	]);
});

test('does not emit uploadProgress after cancelation', withServer, async (t, server, got) => {
	server.post('/', () => {});

	const stream = got.stream.post();

	stream.once('uploadProgress', () => { // 0%
		stream.once('uploadProgress', () => { // 'foo'
			stream.write('bar');

			process.nextTick(() => {
				process.nextTick(() => {
					stream.on('uploadProgress', () => {
						t.fail('Emitted uploadProgress after cancelation');
					});

					stream.destroy();
				});
			});
		});
	});

	stream.write('foo');

	await pEvent(stream, 'close');

	t.pass();
});

test('upload progress - chunk generator with buffer', withServer, async (t, server, got) => {
	server.post('/', uploadEndpoint);

	const events: Progress[] = [];

	await got.post({
		body: chunk(file, 65_536),
		headers: {
			'content-length': file.length.toString(),
		},
	})
		.on('uploadProgress', (event: Progress) => events.push(event));

	checkEvents(t, events, file.length);

	// Ensure we got more than just 0% and 100%
	t.true(events.length > 2, `Expected more than 2 events with chunk, got ${events.length}`);
});

test('upload progress - chunkFromAsync async generator with stream', withServer, async (t, server, got) => {
	server.post('/', uploadEndpoint);

	const events: Progress[] = [];

	await got.post({
		body: chunkFromAsync(stream.Readable.from([file]), 65_536),
		headers: {
			'content-length': file.length.toString(),
		},
	})
		.on('uploadProgress', (event: Progress) => events.push(event));

	checkEvents(t, events, file.length);

	// Ensure we got more than just 0% and 100%
	t.true(events.length > 2, `Expected more than 2 events with chunkFromAsync, got ${events.length}`);
});
